Android 触摸事件分发解析

Android 触摸事件分发解析

前言

Android 中的界面是由一颗 view tree 组成的,从 Activity 开始向下分发触摸事件直到被目标 view 消费,这篇文章将带你详细了解事件从 viewRoot 接受到目标 view 消费的过程

代码基于 Android 7.0

基础知识

事件的传递对象 MotionEvent 有许多类型,通过 getAction 方法可以获取到当前事件的类型,主要有

  • ACTION_DOWN 手指按下事件
  • ACTION_MOVE 手指移动事件
  • ACTION_UP 手指抬起事件
  • ACTION_CANCEL 触摸取消事件

以及一些额外的手势,比如说用于多指操控的 ACTION_POINT_XX 事件等,但绝大部分的控件只需要关心这四个事件就行了,一般的触摸事件如下图所示
触摸事件

Android 的源码量太庞大了,所以在解析过程中会选择性的略去一部分非关键性流程代码

事件分发流程解析

ViewRootImpl 转发

如果想看触摸事件是如何从底层传递到应用层的话,我建议你去看看 gityuan 的关于一系列关于 Input 系统的博客,本文从 Input系统—事件处理全过程 这篇博客中在 ViewRootImpl 接收到数据后 deliverInputEvent 方法开始解析

ViewRootImpl.deliverInputEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
private void deliverInputEvent(QueuedInputEvent q) {
Trace.asyncTraceBegin(Trace.TRACE_TAG_VIEW, "deliverInputEvent",
q.mEvent.getSequenceNumber());
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
}

InputStage stage;
if (q.shouldSendToSynthesizer()) {
stage = mSyntheticInputStage;
} else {
stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
}
// 会有一个 stage 的管道来分发到对应处理事件的窗口
if (stage != null) {
stage.deliver(q);
} else {
finishInputEvent(q);
}
}

简述一下过程,在 InputStage 中会持有下一个 InputStage 的引用,InputStage.deliver 分发时会在 InputStage.onProcess 中处理具体的逻辑,然后根据返回值设置下一个 InputStage.deliver 分发时是否需要处理的状态,知道这条调用链执行完毕,调用 finishInputEvent 回调给系统。沿着调用链查看对应的 onProcess 方法,发现在 ViewPostImeInputStage.onProcss 中会判断事件的来源是否是 SOURCE_CLASS_POINTER,是的话就会调用 ViewPostImeInputStage.processPointerEvent 方法处理手指事件

ViewPostImeInputStage.processPointerEvent 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;

mAttachInfo.mUnbufferedDispatchRequested = false;
mAttachInfo.mHandlingPointerEvent = true;
// 这里会从 view 树的最顶端开始分发这个触摸事件
boolean handled = mView.dispatchPointerEvent(event);
maybeUpdatePointerIcon(event);
maybeUpdateTooltip(event);
mAttachInfo.mHandlingPointerEvent = false;
if (mAttachInfo.mUnbufferedDispatchRequested && !mUnbufferedInputDispatch) {
mUnbufferedInputDispatch = true;
if (mConsumeBatchedInputScheduled) {
scheduleConsumeBatchedInputImmediately();
}
}
return handled ? FINISH_HANDLED : FORWARD;
}

ViewRootImpl 中的 mView 变量其实持有的是 DecorView 的引用,而 DecorView 则是一个 Activity 布局中最顶层的控件,所以 DecorView 是作为一个根节点开发分发事件的

View.dispatchPointerEvent 方法

1
2
3
4
5
6
7
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event);
} else {
return dispatchGenericMotionEvent(event);
}
}

改方法是 final 修饰的,所以该方法无法被子类覆写,而且该方法是 @hide 的,开发者无法直接调用它。当 event 是一个触摸事件是就会调用 dispatchTouchEvent,DecorView 有覆写该方法。

Activity 的相关操作

DecorView.dispatchTouchEvent 方法

1
2
3
4
5
6
7
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// mWindow 是 DecorView 持有的 window 对象,即 Activity 对窗口进行管理的对象
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}

DecorView 会将 dispatchTouchEvent 交由 Window 中 持有的一个 Callback 对象去执行逻辑,去 Window 的唯一子类 PhoneWindow 中查看相关代码,发现这个 Callback 对象实际上就是宿主 Activity,继续追踪到 Activity.dispatchTouchEvent 方法

Activity.dispatchTouchEvent 方法

1
2
3
4
5
6
7
8
9
10
11
12
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 会有一个用户交互触发的回调
onUserInteraction();
}
// 真正地开始沿着 view tree 分发事件
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 事件没有被消费,才会交给 Activity.onTouchEvent 去处理
return onTouchEvent(ev);
}

分发时,event 通过 PhoneWindow.superDispatchTouchEvent 传递到 DecorView.superDispatchTouchEvent 方法中,而 DecorView 调用了 super.dispatchTouchEvent(DecorView 覆写了 dispatchTouchEvent,上文也有提到,所以调用父类 ViewGroup 的 dispatchTouchEvent 方法)

由此可以知道 Activity 的处理逻辑,它会先于所有的 view 接收到触摸事件,在 dispatchTouchEvent 中只有 View 没有消费事件,才会执行 onTouchEvent

ViewGroup 的相关操作

追踪到 ViewGroup.dispatchTouchEvent 方法,这个方法里定义了事件沿着树状结构向下传递的主要规则

在看源码之前需要了解一些基本知识,一个触摸事件的周期由 down 事件开始,正常会以 up 事件结束,在这个周期内如果 view 消费了事件,那么后续事件都会直接传递进来,如果没有则不会接受到后续事件

ViewGroup.dispatchTouchEvent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
 @Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...

// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}

boolean handled = false;
// 根据安全策略过滤部分事件
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;

// down 会作为一个触摸事件周期的起始状态
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 将会抛弃上一个触摸事件周期的消费状态
// 由于 APP 切换,ANR 等原因, up 或者 cancel 事件可能会被抛弃,就会导致状态没有被清理
cancelAndClearTouchTargets(ev);
resetTouchState();
}

// 检查是否被拦截的逻辑,这里会调用到 onInterceptTouchEvent 事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// ViewGroup 是否被禁止拦截事件,这也是 requestDisallowInterceptTouchEvent 方法作用的地方
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果没有被禁止,这里会调用 ViewGroup.onInterceptTouchEvent 方法
intercepted = onInterceptTouchEvent(ev);
// 重置状态防止万一在 onInterceptTouchEvent 里被改变了
ev.setAction(action);
} else {
intercepted = false;
}
} else {
// 如果即不是初始的 down 状态,又没有可以消费的事件的目标 view,已经不需要向下传递了
intercepted = true;
}

// 如果被拦截或者目标消费 view,则可以把这种焦点事件当做普通事件来处理
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}

// 根据 action 是否是 ACTION_CANCEL 状态或者本 View 是否已经设置了取消下一个事件的标志位得到 canceled 标记
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;

// 更新手指按下时的可能存在的消费目标
// 这个标志指代多指按下时是否需要区分手指的不同事件
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;

TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 如果没有被取消或拦截,则会根据状态(一般是 down 事件,也有拆分多指按下事件)进入到查找消费子 view 的阶段
if (!canceled && !intercepted) {

// 查找当前准备被赋予焦点的 view
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;

if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 通常由 down 事件触发,重新寻找新的触摸消费目标
final int actionIndex = ev.getActionIndex();
// 标记消费目标的 pointerIdBits,因为是 int,所以最多只可能有 32 种标记
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;

// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 按照从前到后的顺序遍历子 view 查找到能接受事件的控件
// 构建一个从前到后的有序子 view 集合,遍历也是需要这个顺序,这样就能找到让最前面的消费事件
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);

// 这个操作能使 childWithAccessibilityFocus 在循环中先被处理,如果没有被该 view 消费,则会重新开始循环
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}

// 不可见,正在执行动画的 View 以及事件未发生在 View 区域内的都不会消费事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

// 查询是否在触摸目标链中
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
// 位与操作,可以将该指针新增上去
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}

// 清除可能存在的取消操作
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 该子 View 能消费触摸事件
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// 查找真正的 index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 将 child 包装成 touchTarget 头插到链表
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 标志着找到了新的分发对象,退出循环
alreadyDispatchedToNewTouchTarget = true;
break;
}

// 如果预期焦点 view 无法消费事件,清理焦点,开始普通分发
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}

if (newTouchTarget == null && mFirstTouchTarget != null) {
// 如果没有找到能够接收事件的子 view,那么把指针添加到最近加入的 target
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}

// 开始给 touchTarget 分发
if (mFirstTouchTarget == null) {
// 如果没有目标消费的子 view,追踪代码可以知道会调用 super.dispatchTouchEvent 即把 ViewGroup 当成了普通的 View 处理
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 这里会往 touch targets 分发事件,除非是在重置流程事件中已经处理过分发了,同时还有取消 touchTarget 的相关工作
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 如果被拦截,事件是不会被分发到子 View 中去的
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 如果取消或被拦截,那对应的 touch target 会被移除
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}

// 根据状态跟新 touch target 的列表
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 清除所有的 touch target,回复状态,这里一个触摸周期会结束
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
// 这里会清除对应 idBits 的 touchTarget
// 清除时会有个逻辑,就是如果 target.pointerIdBits 清除了对应的 idBits 后没其他标记,则会移除这个 target,对象被回收
removePointersFromTouchTargets(idBitsToRemove);
}
}

if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}

上述流程中有三处调用了 dispatchTransformedTouchEvent 方法,从传参和方法字面意义可知,里面执行的是将处理后的 TouchEvent 传给 child.dispatchTouchEvent 来查看事件是否会被消费

ViewGroup.dispatchTransformedTouchEvent 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;

// 如果是本 View 取消了下一个事件或者 ACTION_CANCEL,统一分发 ACTION_CANCEL
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
//此时会把 ViewGroup 当做一个普通 View 处理
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}

// 对事件中操作的手指做位做校验
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

// 抛弃校验不通过的一些前后状态不一致的情况
if (newPointerIdBits == 0) {
return false;
}

// 对点击事件的位置做处理,以便子 view 接受到的数据是相对位置
// 如果 view 处理的手指数是和事件的手指数相同的,那么我们不需要拆分事件,只需要将处理后的事件复原就可以了
// 如果不相同,那么我们就需要一个 transformedEvent 去复制得到不可逆的拆分变换
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);

handled = child.dispatchTouchEvent(event);

event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}

if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}

handled = child.dispatchTouchEvent(transformedEvent);
}

// 回收,对这种频繁创建的对象有对象池管理防止内存抖动
transformedEvent.recycle();
return handled;
}

可以看到如果传入的 child 为空,则会调用 ViewGroup 的父类方法的 dispatchTouchEvent,相当于把 ViewGroup 当成普通的 View 处理了。如果传递给子 view 那么会根据子 view 能够处理对应的手指来拆分事件,并对事件的位置进行裁剪,使得子 view 获取到的事件位置是相对位置。

这就是 ViewGroup 的分发逻辑,简单来说一个触摸周期的 DOWN 事件来时,会判断是否取消或者拦截,都没有的话就根据从前到后的 view 顺序遍历子 view 找到能消费的 view 并记录下来,等到 MOVE 等后续事件来时就不需要再遍历了,只需要找到对应的消费了 DOWN 事件的 view 直接传递给他就行,直到 UP 或者 CANCEL 事件清除记录下来的 view。等到下一个周期从新开始。

View的相关操作

上节提到当 ViewGroup 在 DOWN 事件被拦截或者取消,或者找不到可以消费事件的子 view,那他就会调用 View.dispatchTouchEvent,或者当事件传递到最底层时也是由 View.dispatchTouchEvent 去处理的

View.dispatchTouchEvent 方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
public boolean dispatchTouchEvent(MotionEvent event) {
// 会优先处理焦点事件
if (event.isTargetAccessibilityFocus()) {
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
event.setTargetAccessibilityFocus(false);
}

boolean result = false;

if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}

if (onFilterTouchEventForSecurity(event)) {
// 滑动块的处理事件
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// 会优先处理 OnTouchListener
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}

// 没有处理才会轮到 onTouchEvent 方法
if (!result && onTouchEvent(event)) {
result = true;
}
}

if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}

// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}

return result;
}

View 的 dispatchTouchEvent 相对比较简单,只是操作了一些额外的滑动事件处理,只要逻辑在于如果有 OnTouchListener 就交给 OnTouchListener.onTouch 处理,不消费则再去通过 onTouchEvent 方法处理,如果都没有消费意味着返回 false,那后续的事件是不会传进来的

可以用一张图来描述这个过程

事件分发图

结语

一个相对完整的触摸事件分发流程就解析完了,不仅仅是停留于纸面上的 API 调用关系,相信会对你自定义 ViewGroup 如何处理事件分发,解决滑动冲突会有很大的帮助